using System;
using System.Linq;
using System.Linq.Expressions;

/// <summary>
/// Convenience methods for Monad<A, M>
/// </summary>
/// <remarks>
/// It's easier to understand the AST when
/// our syntax gives fun-name, arg0, ..., argn
/// compared to arg0, fun-name, arg1, ..., argn
/// </remarks>
public class Monad {
    public static Monad<B, M> Star<A, B, M>(Monad<A, M> ma, Func<A, Monad<B, M>> f) where M : MonadImpl<M>, new() {
        return ma.Star<B>(f);
    }
    public static Monad<A, M> Unit<A, M>(A a) where M : MonadImpl<M>, new() {
        return Unit<A, M>(() => a);
    }
    public static Monad<A, M> Unit<A, M>(Func<A> a) where M : MonadImpl<M>, new() {
        return new Monad<A, M>() { a = a };
    }
    public static A Just<A, M>(Monad<A, M> ma) where M : MonadImpl<M>, new() {
        return ma.a();
    }
}
public class MonadImpl<M> where M : MonadImpl<M>, new() {
    public virtual Monad<B, M> Star<A, B>(Monad<A, M> ma, Func<A, Monad<B, M>> f) {
        return f(ma.Just());
    }
}
public class Monad<T, M> where M : MonadImpl<M>, new() {
    public M impl = new M();
    public T Just() {
        return a();
    }
    internal protected Func<T> a;
    public Monad<B, M> Star<B>(Func<T, Monad<B, M>> f) {
        return impl.Star<T, B>(this, f);
    }
    public static Monad<A, M> Unit<A>(A a) {
        return new Monad<A, M>() { a = () => a };
    }        
}

public class IdentityMonadImpl<M> : MonadImpl<M> where M : IdentityMonadImpl<M>, new() {}
public class IdentityMonadImpl : IdentityMonadImpl<IdentityMonadImpl> {}
public class IdentityMonad<T> : Monad<T, IdentityMonadImpl> {
}

public class ExceptionMonadImpl<M> : MonadImpl<M> where M : ExceptionMonadImpl<M>, new() {
    private bool eP = false;
    private Exception e;
    public override Monad<B, M> Star<A, B>(Monad<A, M> ma, Func<A, Monad<B, M>> f) {
        if (ma.impl.eP) return ehelper<B>(ma.impl.e);
        try {
            return base.Star(ma, f);
        }
        catch (Exception e) {
            return ehelper<B>(e);
        }
    }
    protected Monad<B, M> ehelper<B>(Exception e) {
        var result = new Monad<B, M>();
        result.impl.eP = true;
        result.impl.e = e;
        return result;
    }
}
public class ExceptionMonadImpl : ExceptionMonadImpl<ExceptionMonadImpl> {}
public class ExceptionMonad<T> : Monad<T, ExceptionMonadImpl> {
}

public class MaybeMonadImpl<M> : MonadImpl<M> where M : MaybeMonadImpl<M>, new() {
    private bool nilp = false;
    public override Monad<B, M> Star<A, B>(Monad<A, M> ma, Func<A, Monad<B, M>> f) {
        if (ma.impl.nilp) return nil<B, M>();
        var a = ma.a();
        if (a == null) return nil<B, M>();
        return f(a);
    }
    private Monad<B, M> nil<B, M>() where M : MaybeMonadImpl<M>, new() {
        var mb = new Monad<B, M>() { a = () => default(B) };
        mb.impl.nilp = true;
        return mb;
    }
}
public class MaybeMonadImpl : MaybeMonadImpl<MaybeMonadImpl> {}
public class MaybeMonad<T> : Monad<T, MaybeMonadImpl> {
    private static Expression andandifyme(MemberExpression me) {
        var objectParameter = Expression.Parameter(me.Expression.Type, "target");
        var parameters = new ParameterExpression [] {objectParameter};
        //TODO: simplify by using the parameter f rather than by duplicating
        // the subexpression: andandify(me.Expression) in slotValue and
        // in the Just of Star of Unit below (the subexpression could occur
        // solely in slotValue, and then its value could be used by
        // dereferencing f in the Just of Star of Unit.
        var slotValue =
            andandifyinr(
                Expression.Invoke(
                    Expression.Lambda(
                        Expression.MakeMemberAccess(
                            objectParameter,
                            me.Member),
                        parameters),
                    andandify(me.Expression)));
        var f = Expression.Parameter(me.Expression.Type, "f");
        return
            Expression.Call(
                typeof(Monad),
                "Just",
                new Type [] { me.Type, typeof(MaybeMonadImpl) },
                Expression.Call(
                    typeof(Monad),
                    "Star",
                    new Type [] { me.Expression.Type, me.Type, typeof(MaybeMonadImpl) },
                    Expression.Call(
                        typeof(Monad),
                        "Unit",
                        new Type [] { me.Expression.Type, typeof(MaybeMonadImpl) },
                        andandify(me.Expression)),
                    Expression.Lambda(
                        Expression.Call(
                            typeof(Monad),
                            "Unit",
                            new Type [] { me.Type, typeof(MaybeMonadImpl) },
                            slotValue),
                        f)));
    }
    private static Expression andandifyi(InvocationExpression i) {
        return
            andandifyinr(
                Expression.Invoke(
                    andandify(i.Expression),
                    i.Arguments));
    }
    private static Expression andandifyinr(InvocationExpression i) {
        var l = i.Expression;
        var f = Expression.Parameter(l.Type, "f");
        return
            Expression.Call(
                typeof(Monad),
                "Just",
                new Type [] { i.Type, typeof(MaybeMonadImpl) },
                Expression.Call(
                    typeof(Monad),
                    "Star",
                    new Type [] { l.Type, i.Type, typeof(MaybeMonadImpl) },
                    Expression.Call(
                        typeof(Monad),
                        "Unit",
                        new Type [] { l.Type, typeof(MaybeMonadImpl) },
                        l),
                    Expression.Lambda(
                        Expression.Call(
                            typeof(Monad),
                            "Unit",
                            new Type [] { i.Type, typeof(MaybeMonadImpl) },
                            i),
                        f)));
    }
    private static LambdaExpression andandifyl(LambdaExpression l) {
        return
            Expression.Lambda(
                andandify(l.Body),
                Enumerable.Select(
                    l.Parameters,
                    p => (ParameterExpression)andandify(p)).ToArray());
    }
    private static Expression andandify(Expression expr) {
        var me = expr as MemberExpression;
        if (me != null) {
            return andandifyme(me);
        }
        var i = expr as InvocationExpression;
        if (i != null) {
            return andandifyi(i);
        }
        var l = expr as LambdaExpression;
        if (l != null) {
            return andandifyl(l);
        }
        return expr;
    }
    public static U Andand<U>(Expression<Func<U>> expr) {
        var aexpr = (Expression<Func<U>>)andandify(expr);
        try {
            return (U)aexpr.Compile().DynamicInvoke();
        }
        catch (Exception e) {
            throw new Exception(
                          String.Format(
                               "trouble evaluating {0}", 
                               aexpr),
                          e);
        }
    }
}
